package com.beust.jcommander;

import com.beust.jcommander.internal.Lists;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.List;

/**
 * Encapsulate a field or a method annotated with @Parameter or @DynamicParameter
 */
public class Parameterized {

    // Either a method or a field
    private Field m_field;
    private Method m_method;
    private Method m_getter;

    // Either of these two
    private WrappedParameter m_wrappedParameter;
    private ParametersDelegate m_parametersDelegate;

    public Parameterized(WrappedParameter wp, ParametersDelegate pd,
                         Field field, Method method) {
        m_wrappedParameter = wp;
        m_method = method;
        m_field = field;
        if (m_field != null) {
            m_field.setAccessible(true);
        }
        m_parametersDelegate = pd;
    }

    public static List<Parameterized> parseArg(Object arg) {
        List<Parameterized> result = Lists.newArrayList();

        Class<? extends Object> cls = arg.getClass();
        while (!Object.class.equals(cls)) {
            for (Field f : cls.getDeclaredFields()) {
                Annotation annotation = f.getAnnotation(Parameter.class);
                Annotation delegateAnnotation = f.getAnnotation(ParametersDelegate.class);
                Annotation dynamicParameter = f.getAnnotation(DynamicParameter.class);
                if (annotation != null) {
                    result.add(new Parameterized(new WrappedParameter((Parameter) annotation), null,
                            f, null));
                } else if (dynamicParameter != null) {
                    result.add(new Parameterized(new WrappedParameter((DynamicParameter) dynamicParameter), null,
                            f, null));
                } else if (delegateAnnotation != null) {
                    result.add(new Parameterized(null, (ParametersDelegate) delegateAnnotation,
                            f, null));
                }
            }
            cls = cls.getSuperclass();
        }

        // Reassigning
        cls = arg.getClass();
        while (!Object.class.equals(cls)) {
            for (Method m : cls.getDeclaredMethods()) {
                Annotation annotation = m.getAnnotation(Parameter.class);
                Annotation delegateAnnotation = m.getAnnotation(ParametersDelegate.class);
                Annotation dynamicParameter = m.getAnnotation(DynamicParameter.class);
                if (annotation != null) {
                    result.add(new Parameterized(new WrappedParameter((Parameter) annotation), null,
                            null, m));
                } else if (dynamicParameter != null) {
                    result.add(new Parameterized(new WrappedParameter((DynamicParameter) annotation), null,
                            null, m));
                } else if (delegateAnnotation != null) {
                    result.add(new Parameterized(null, (ParametersDelegate) delegateAnnotation,
                            null, m));
                }
            }
            cls = cls.getSuperclass();
        }

        return result;
    }

    public WrappedParameter getWrappedParameter() {
        return m_wrappedParameter;
    }

    public Class<?> getType() {
        if (m_method != null) {
            return m_method.getParameterTypes()[0];
        } else {
            return m_field.getType();
        }
    }

    public String getName() {
        if (m_method != null) {
            return m_method.getName();
        } else {
            return m_field.getName();
        }
    }

    public Object get(Object object) {
        try {
            if (m_method != null) {
                if (m_getter == null) {
                    m_getter = m_method.getDeclaringClass()
                            .getMethod("g" + m_method.getName().substring(1),
                                    new Class[0]);
                }
                return m_getter.invoke(object);
            } else {
                return m_field.get(object);
            }
        } catch (SecurityException e) {
            throw new ParameterException(e);
        } catch (NoSuchMethodException e) {
            // Try to find a field
            String name = m_method.getName();
            String fieldName = Character.toLowerCase(name.charAt(3)) + name.substring(4);
            Object result = null;
            try {
                Field field = m_method.getDeclaringClass().getDeclaredField(fieldName);
                if (field != null) {
                    field.setAccessible(true);
                    result = field.get(object);
                }
            } catch (NoSuchFieldException ex) {
                // ignore
            } catch (IllegalAccessException ex) {
                // ignore
            }
            return result;
        } catch (IllegalArgumentException e) {
            throw new ParameterException(e);
        } catch (IllegalAccessException e) {
            throw new ParameterException(e);
        } catch (InvocationTargetException e) {
            throw new ParameterException(e);
        }
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((m_field == null) ? 0 : m_field.hashCode());
        result = prime * result + ((m_method == null) ? 0 : m_method.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Parameterized other = (Parameterized) obj;
        if (m_field == null) {
            if (other.m_field != null)
                return false;
        } else if (!m_field.equals(other.m_field))
            return false;
        if (m_method == null) {
            if (other.m_method != null)
                return false;
        } else if (!m_method.equals(other.m_method))
            return false;
        return true;
    }

    public boolean isDynamicParameter(Field field) {
        if (m_method != null) {
            return m_method.getAnnotation(DynamicParameter.class) != null;
        } else {
            return m_field.getAnnotation(DynamicParameter.class) != null;
        }
    }

    public void set(Object object, Object value) {
        try {
            if (m_method != null) {
                m_method.invoke(object, value);
            } else {
                m_field.set(object, value);
            }
        } catch (IllegalArgumentException ex) {
            throw new ParameterException(ex);
        } catch (IllegalAccessException ex) {
            throw new ParameterException(ex);
        } catch (InvocationTargetException ex) {
            // If a ParameterException was thrown, don't wrap it into another one
            if (ex.getTargetException() instanceof ParameterException) {
                throw (ParameterException) ex.getTargetException();
            } else {
                throw new ParameterException(ex);
            }
        }
    }

    public ParametersDelegate getDelegateAnnotation() {
        return m_parametersDelegate;
    }

    public Type getGenericType() {
        if (m_method != null) {
            return m_method.getGenericParameterTypes()[0];
        } else {
            return m_field.getGenericType();
        }
    }

    public Parameter getParameter() {
        return m_wrappedParameter.getParameter();
    }

    /**
     * @return the generic type of the collection for this field, or null if not applicable.
     */
    public Type findFieldGenericType() {
        if (m_method != null) {
            return null;
        } else {
            if (m_field.getGenericType() instanceof ParameterizedType) {
                ParameterizedType p = (ParameterizedType) m_field.getGenericType();
                Type cls = p.getActualTypeArguments()[0];
                if (cls instanceof Class) {
                    return cls;
                }
            }
        }

        return null;
    }

    public boolean isDynamicParameter() {
        return m_wrappedParameter.getDynamicParameter() != null;
    }

}
